﻿//-----------------------------------------------------------------------
// <copyright file="XAMLFile.cs" company="INL sprl">
//     Copyright 2009-2010 INL sprl.
// </copyright>
// Fabrice avaux : Date de création 01/10/2009
//-----------------------------------------------------------------------
/* 0.2 22/11/2009
 * try to get x:Name for element name (qualified attributed)
 * use x:Uid as Name if exist
 * Generate a x:Key in place of x:Name in .Resources (WPF requirement)
 * Try to convert Content Element to Content Attribute
 * Add a second pass to Add Name attribute (the code method)
 */
/* 0.3 24/11/2009
 * generate the class associated with resx (do not depend on external tools)
 * use codedom to generate the class for code method
*/
/* 0.4 29/11/2009
 * Add Generator of Uid
 * Add Binding method for text in resource for the code method
 */
/* 0.5 16/01/2010
 * Refactor code for generating name and uid
 * Add logging of all ResX operation
 * Update existing string in ResX file (the new default mode)
 * Modify the code to generate the name of resource name
 */
/* 0.5.1 17/01/2010
 * Add prefix to ignore
 * The binding can be customizable
 */

/*
 * basé sur les conseils http://www.silverlighttips.com/?tag=/localization
 * utilise le générateur de resource publique http://www.guysmithferrier.com/post/2008/09/Silverlight-And-Strongly-Typed-Resource-Classes.aspx
*/
namespace ParseXAML
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Xml;

    /// <summary>
    /// Classe qui permet de lire un fichier XAML, de le modifier et génère le fichier .resx
    /// </summary>
    public class XAMLFile
    {
        /// <summary>
        /// Répertoire où sont créés les fichiers ressources.
        /// </summary>
        public const string DirResources = "App_Resources";

        /// <summary>
        /// Track if the Alias for resource is created
        /// </summary>
        internal bool IsAliasResourceGenerated;

        /// <summary>
        /// Namespace of UserControl (extracted from the XAML)
        /// </summary>
        internal string UserControlNamespace;

        /// <summary>
        /// Namespace of the proxy class for the resource
        /// </summary>
        internal string ResXNamespace;

        /// <summary>
        /// Name of the proxy class for the resource
        /// </summary>
        internal string ResXClassName;

        /// <summary>
        /// Contient les resources à générer (d'abord les existantes + les nouvelles)
        /// </summary>
        internal List<ResourceDataItem> ListResource = new List<ResourceDataItem>();

        /// <summary>
        /// All the namespace associations of the root Element
        /// </summary>
        internal NamespaceAssociations MyNamespaceAssociations;

        /// <summary>
        /// Contient les éléments à ajouter dans la méthode Translat
        /// </summary>
        internal List<TranslatLine> TranslatLines = new List<TranslatLine>();

        /// <summary>
        /// Code du fichier code behind (.translat.extension)
        /// </summary>
        internal string CodeFileFlat;

        /// <summary>
        /// The x:Name in document
        /// </summary>
        internal NamesAndUidsBase Names;

        /// <summary>
        /// The Uid in document
        /// </summary>
        internal NamesAndUidsBase Uids;

        /// <summary>
        /// Number of properties in resX
        /// </summary>
        internal int NumberProperties;

        /// <summary>
        /// Number of Uid Added
        /// </summary>
        internal int NumberAddUid;

        /// <summary>
        /// Number of Name Added
        /// </summary>
        internal int NumberAddName;

        /// <summary>
        /// Number of binding created
        /// </summary>
        internal int NumberAddBinding;

        /// <summary>
        /// Namespace alias of the resx classes
        /// </summary>
        protected const string NamespaceAliasForOurResource = "INLResources";

        /// <summary>
        /// Root Element of the document
        /// </summary>
        protected XmlElement rootElement;

        /// <summary>
        /// Nom de la source du binding dans le fichier XAML
        /// partie Source=xxx du Binding 
        /// </summary>
        protected string resourceName;

        /// <summary>
        /// Alias for namespace XAML (often x)
        /// </summary>
        protected string ns_x;

        /// <summary>
        /// Document source (XAML)
        /// </summary>
        protected XmlDocument doc;

        /// <summary>
        /// Extension of generated file
        /// </summary>
        protected string extension;

        /// <summary>
        /// Depend on the language, on some Language like C# directory is used as part of NameSpace
        /// </summary>
        protected bool isDirectoryAsNamespace;

        /// <summary>
        /// XAML when loaded
        /// </summary>
        protected string originalXAML;

        /// <summary>
        /// Contexte global
        /// </summary>
        protected Global global;

        /// <summary>
        /// Array of all prefix ignored
        /// </summary>
        protected string[] ignoreStringBeginBy;

        /// <summary>
        /// Array of params to replace for the binding method
        /// </summary>
        protected Dictionary<string, string> replaceParams = new Dictionary<string, string>();

        /// <summary>
        /// Initializes a new instance of the XAMLFile class.
        /// </summary>
        /// <param name="global">Une instance des paramètres globaux</param>
        /// <param name="extension">File extension (.cs, .vb, ...)</param>
        public XAMLFile(Global global, string extension)
        {
            this.global = global;
            this.extension = extension;
            this.isDirectoryAsNamespace = CodeDOM.PartialResource.IsDirectoryAsNamespace(extension);
        }

        /// <summary>
        /// Calcul le nom du fichier ressource généré (xxx.xaml => yyy\xxx.xaml.resx)
        /// </summary>
        /// <param name="fileName">Nom complet du fichier xaml</param>
        /// <returns>Le nom complet du fichier resx</returns>
        public static string GetOutputFileName(string fileName)
        {
            return Path.Combine(Path.GetDirectoryName(fileName), DirResources + "\\" + Path.GetFileName(fileName) + ".resx");
        }

        /// <summary>
        /// Lance le traitement du document contenu dans data.
        /// </summary>
        /// <param name="data">Contenu du fichier xaml</param>
        /// <param name="fileName">Nom complet du fichier xaml</param>
        public void Go(string data, string fileName)
        {
            XmlDocument doc = new XmlDocument();
            doc.PreserveWhitespace = true;
            doc.InnerXml = data;
            this.Go(doc, fileName);
        }

        /// <summary>
        /// Lance le traitement du fichier
        /// </summary>
        /// <param name="fileName">Nom du fichier à traiter</param>
        /// <returns>Renvoie une liste de fichier à modifier.</returns>
        public Outputs Go(string fileName)
        {
            XmlDocument doc = new XmlDocument();
            doc.PreserveWhitespace = true;
            doc.Load(fileName);
            ////make a copy of document to detect change
            StringWriter sw2 = new StringWriter();
            XmlTextWriter xw = new XmlTextWriter(sw2);
            doc.Save(xw);
            this.originalXAML = sw2.ToString();
            this.Go(doc, fileName);

            bool codeFile = this.global.IsGenerateCodeBehind || this.global.GenerateWrapperClassForResource;

            this.global.AddMessage(Message.eType.Information, "{0} new bindinds, {1} new Uids, {2} new Names, {3} resources", this.NumberAddBinding, this.NumberAddUid, this.NumberAddName, this.NumberProperties);

            Outputs output = new Outputs();

            if (codeFile)
            {
                string codeFileName = GetOutputFileNameCode(fileName, this.extension);
                output.Add(new StringOutput(this.CodeFileFlat, eType.Code, codeFileName));
                this.global.AddMessage(Message.eType.Information, "{0} is generated", codeFileName);
            }
            ////try to serialize XAML to compare with original
            sw2 = new StringWriter();
            xw = new XmlTextWriter(sw2);
            doc.Save(xw);
            if (this.originalXAML != sw2.ToString())
            {
                this.global.AddMessage(Message.eType.Information, "{0} is modified", fileName);
                output.Add(new XAMLOutput(doc, eType.XAML, fileName));
            }

            return output;
        }

        /// <summary>
        /// Lance le traitement du document
        /// </summary>
        /// <remarks>
        /// La base du nom du document servira à créé le fichier .resx
        /// </remarks>
        /// <param name="doc">Document à traiter</param>
        /// <param name="fileName">Nom du document</param>
        public void Go(XmlDocument doc, string fileName)
        {
            if (this.global.IgnoreStringBeginBy != null)
            {
                this.ignoreStringBeginBy = this.global.IgnoreStringBeginBy.Split(new char[] { ';' });
            }

            this.doc = doc;
            this.rootElement = doc.DocumentElement;

            this.MyNamespaceAssociations = new NamespaceAssociations(this.rootElement);

            // namespace often alias as "x" in XAML
            this.ns_x = this.rootElement.GetPrefixOfNamespace(NamespaceAssociations.nsXamlName);

            XmlAttribute xclass = this.rootElement.GetAttributeNode("Class", NamespaceAssociations.nsXamlName);
            if (xclass == null)
            {
                return;
            }

            // Recup the namespace and the className of the class associated with the XAML
            string className = xclass.InnerXml;
            int classNamePt = className.LastIndexOf('.');
            this.UserControlNamespace = className.Substring(0, classNamePt); // hope to be the namespace of the UserControl or other
            className = className.Substring(classNamePt + 1); // hope to be the class name

            // nom de la ressource dans le fichier XAML
            this.resourceName = className + "res";

            // Calculate the namespace of the resx file
            this.ResXClassName = Path.GetFileName(fileName).Replace('.', '_');
            if (this.global.GenerateWrapperClassForResource)
            {
                this.ResXClassName = CodeDOM.PartialResource.GetResXClassName(this.ResXClassName);
                this.ResXNamespace = this.UserControlNamespace;
            }
            else
            {
                if (string.IsNullOrEmpty(this.global.ResourceNamespace2))
                {
                    this.ResXNamespace = this.UserControlNamespace; // probably namespace of the generated designer
                }
                else
                {
                    this.ResXNamespace = this.global.ResourceNamespace2; // probably namespace of the generated designer
                }

                if (this.isDirectoryAsNamespace)
                {
                    this.ResXNamespace = this.ResXNamespace + "." + DirResources;
                }
            }

            this.Names = new Names(this.global, this.rootElement);

            this.Uids = new Uids(this.global, this.rootElement);

            string resxFileName = GetOutputFileName(fileName);

            // nom de la ressource embeddé dans l'assembly
            string resourceNameInAssemblyAfterGlobal = null;
            if (this.isDirectoryAsNamespace)
            {
                string destinationDir = System.IO.Path.GetDirectoryName(resxFileName);
                string relativePath = PathUtil.GetRelativePath(this.global.Folderpath, destinationDir);
                if (relativePath != destinationDir)
                {
                    resourceNameInAssemblyAfterGlobal = relativePath.Replace(System.IO.Path.DirectorySeparatorChar, '.');
                }
                else
                {
                    this.global.AddMessage(Message.eType.Warning, Trans.ResourceFileNotInSubDirOfProjet);
                    int pos = this.UserControlNamespace.LastIndexOf(".");
                    if (pos >= 0)
                    {
                        resourceNameInAssemblyAfterGlobal = this.UserControlNamespace.Substring(pos + 1) + "." + DirResources;
                    }
                    else
                    {
                        resourceNameInAssemblyAfterGlobal = DirResources;
                    }
                }
            }

            string resourceName = Path.GetFileName(fileName);
            if (string.IsNullOrEmpty(resourceNameInAssemblyAfterGlobal))
            {
                resourceNameInAssemblyAfterGlobal = resourceName;
            }
            else
            {
                resourceNameInAssemblyAfterGlobal = resourceNameInAssemblyAfterGlobal + "." + resourceName;
            }

            string resourceNameInAssembly = this.global.DefaultNamespace + "." + resourceNameInAssemblyAfterGlobal;

            this.replaceParams.Add("ResourceName", this.resourceName);
            this.replaceParams.Add("DefaultNamespace", this.global.DefaultNamespace);
            this.replaceParams.Add("ResourceFullNameWithOutGlobal", resourceNameInAssemblyAfterGlobal);
            this.replaceParams.Add("ResourceFullName", resourceNameInAssembly);
            this.replaceParams.Add("AssemblyName", this.global.TargetAssemblyName);

            this.TranslateNode(this.rootElement, false);

            this.global.AddLogMessage(LogMessage.eType.Information, Trans.ScanFile, resxFileName, System.DateTime.Now);
            ResxGenerator.SaveResourceFile(this.global, resxFileName, this.ListResource);
            this.global.AddLogMessage(LogMessage.eType.Information, "{0} new bindinds, {1} new Uids, {2} new Names, {3} resources", this.NumberAddBinding, this.NumberAddUid, this.NumberAddName, this.NumberProperties);

            StringBuilder sb = new StringBuilder();
            CodeDOM.PartialResource p = new ParseXAML.CodeDOM.PartialResource(this.global, this.extension);
            if (this.CheckForResourceNode())
            {
                // generate only the wrapper for resx
                sb.Append(p.GenCode(className, this.UserControlNamespace, this.ResXClassName, this.ResXNamespace, resourceNameInAssembly, resxFileName));
                sb.AppendLine();
            }

            if (this.global.IsGenerateCodeBehind)
            {
                // generate wrapper and Translate Method
                sb.Append(p.GenCode2(className, this.UserControlNamespace, this.TranslatLines, resourceNameInAssembly));
            }

            this.CodeFileFlat = sb.ToString();
        }

        /// <summary>
        /// Calcul le nom du fichier code généré (xxx.xaml => xxx.translat.cs
        /// </summary>
        /// <param name="fileName">Nom complet du fichier xaml</param>
        /// <param name="extension">Extension for the file (.cs,.vb,...)</param>
        /// <returns>Le nom complet du fichier de code</returns>
        protected static string GetOutputFileNameCode(string fileName, string extension)
        {
            return String.Concat(System.IO.Path.ChangeExtension(fileName, ".xaml.translat"), extension);
        }

        /// <summary>
        /// Check if our node resource is present.
        /// </summary>
        /// <returns>True if the node resource is present</returns>
        private bool CheckForResourceNode()
        {
            string resourceNamespaceFound = this.rootElement.GetNamespaceOfPrefix(NamespaceAliasForOurResource);
            return !string.IsNullOrEmpty(resourceNamespaceFound);
        }

        /// <summary>
        /// Compatibility with version 0.1 and 0.2
        /// </summary>
        /// <param name="resourceClassName">Name of resource class</param>
        /// <param name="resourceNamespace">Namespace of the class</param>
        private void CleanResourceNode(string resourceClassName, string resourceNamespace)
        {
            XmlNamespaceManager nsmgr = new XmlNamespaceManager(this.doc.NameTable);

            nsmgr.AddNamespace(this.ns_x, NamespaceAssociations.nsXamlName);

            XmlElement myresourceNode = this.rootElement.SelectSingleNode("descendant::*[@" + this.ns_x + ":Key='" + this.resourceName + "']", nsmgr) as XmlElement;
            if (myresourceNode == null)
            {
                return;
            }

            if ((myresourceNode.LocalName != resourceClassName) || (myresourceNode.Prefix != NamespaceAliasForOurResource))
            {
                myresourceNode.ParentNode.RemoveChild(myresourceNode);
            }
        }

        /// <summary>
        /// Create the alias in the xaml :
        /// 1) add xmlns:INLResources="clr-namespace:WPFTestC" in the root element
        /// 2) add &lt;INLResources:xxx_xamlLocal x:Key="xxxres" /&gt; in the resources element of the root element
        /// </summary>
        /// <param name="resourceClassName">Name of the Resource Manager class</param>
        /// <param name="resourceNamespace">Namespace of the Resource Manager class</param>
        private void GenerateAliasForResource(string resourceClassName, string resourceNamespace)
        {
            if (this.IsAliasResourceGenerated)
            {
                return;
            }

            this.CleanResourceNode(resourceClassName, resourceNamespace);

            XmlNamespaceManager nsmgr = new XmlNamespaceManager(this.doc.NameTable);

            string nsResourceName = string.Concat("clr-namespace:", resourceNamespace);

            string resourceNamespaceFound = this.rootElement.GetNamespaceOfPrefix(NamespaceAliasForOurResource);

            // if the alias name is not found or is different
            if (string.IsNullOrEmpty(resourceNamespace) || (resourceNamespace != resourceNamespaceFound))
            {
                this.rootElement.SetAttribute("xmlns:" + NamespaceAliasForOurResource, nsResourceName);
            }

            nsmgr.AddNamespace(NamespaceAliasForOurResource, nsResourceName);

            // Search for ressources
            XmlElement resourceNode = this.rootElement.SelectSingleNode("descendant::*[local-name() = '" + this.rootElement.Name + ".Resources']") as XmlElement;
            if (resourceNode == null)
            {
                resourceNode = this.doc.CreateElement(this.rootElement.Name + ".Resources", this.rootElement.NamespaceURI);
                this.rootElement.PrependChild(resourceNode);
            }

            // Create or update the element in .resources to alias the class resx with name resourceName
            XmlElement myresourceNode = resourceNode.SelectSingleNode("descendant::" + NamespaceAliasForOurResource + ":" + resourceClassName, nsmgr) as XmlElement;
            if (myresourceNode == null)
            {
                myresourceNode = this.doc.CreateElement(NamespaceAliasForOurResource, resourceClassName, nsResourceName);
                resourceNode.PrependChild(myresourceNode);
            }

            myresourceNode.SetAttribute("Key", NamespaceAssociations.nsXamlName, this.resourceName);

            this.IsAliasResourceGenerated = true;
        }

        /// <summary>
        /// Effectue le scan du noeud et des sous noeuds
        /// </summary>
        /// <param name="node">Noeud à analyser</param>
        /// <param name="isInResource">Indique si le noeud est dans une ressource (=> binding)</param>
        private void TranslateNode(XmlElement node, bool isInResource)
        {
            isInResource = isInResource || node.LocalName.EndsWith(".Resources", StringComparison.OrdinalIgnoreCase);
            bool generateBinding = isInResource || !this.global.IsGenerateCodeBehind; // binding in resource (or not by code)

            foreach (var n in node.ChildNodes)
            {
                if (n is XmlElement)
                {
                    this.TranslateNode((XmlElement)n, isInResource);
                }
            }

            // recherche le namespace associé à l'élement actuel
            NamespaceAssociation na = this.MyNamespaceAssociations.GetNamespaceAssociation(node.Prefix);
            if (na == null)
            {
                return;
            }

            string fullTypeName = string.Concat(na.NamespaceName + "." + node.LocalName);
            List<string> lc;
            lc = this.global.LocalizableControls.GetLocalizableProperties(na, fullTypeName);
            if (lc == null)
            {
                return;
            }

            WorkingNode workingNode = new WorkingNode(node, this, this.global);

            // no uid, no name for now, we do not implement nothing 
            if (!workingNode.HasName && !workingNode.HasUid)
            {
                if (!(this.global.CreateNameAttribute || this.global.CreateUidAttribute))
                {
                    return;
                }
            }

            // Convert some content node to attribute
            if (node.HasChildNodes && !String.IsNullOrEmpty(node.InnerText) /*&& (global.TryToConvertElementToContent)*/)
            {
                bool isTextAttribute = this.global.ElementContentToText.IndexOf(fullTypeName) != -1;
                bool isContentAttribute = this.global.ElementContentToContent.IndexOf(fullTypeName) != -1;
                if (isTextAttribute || isContentAttribute)
                {
                    // search for internal node
                    int nodeElement = 0;
                    foreach (XmlNode p in node.ChildNodes)
                    {
                        if (p is XmlElement)
                        {
                            nodeElement++;
                        }
                    }

                    if (nodeElement == 0)
                    {
                        string attributeName = isTextAttribute ? "Text" : "Content";
                        XmlAttribute xl = null;
                        workingNode.CreateUidOrNameIfNecessary(isInResource);
                        string value = node.InnerXml; // save the value before we empty node
                        if (generateBinding)
                        { // we need to convert content to attribute for binding
                            xl = node.SetAttributeNode(attributeName, null);
                            node.IsEmpty = true;
                        }

                        this.TranslateAttribute(workingNode, attributeName, xl, value, generateBinding, isInResource);
                    }
                }
            }

            foreach (var l in lc)
            {
                XmlAttribute xl = node.GetAttributeNode(l);
                if (xl != null)
                {
                    string value = xl.InnerXml;
                    if (!string.IsNullOrEmpty(value))
                    {
                        this.TranslateAttribute(workingNode, l, xl, value, generateBinding, isInResource);
                    }
                }
            }
        }

        /// <summary>
        /// Translate the attribute (add to resource, change the value, ...)
        /// </summary>
        /// <param name="workingNode">Node to translate</param>
        /// <param name="attributeName">Name of the attribute to translate</param>
        /// <param name="xl">Attribute to translate</param>
        /// <param name="value">Text to translate</param>
        /// <param name="byBinding">True if use the binding method else the code method</param>
        /// <param name="isInResource">Indicate if the node is in resource node</param>
        private void TranslateAttribute(WorkingNode workingNode, string attributeName, XmlAttribute xl, string value, bool byBinding, bool isInResource)
        {
            if (value.StartsWith("{Binding"))
            {
                return;
            }

            if (this.ignoreStringBeginBy != null)
            {
                foreach (var p in this.ignoreStringBeginBy)
                {
                    if (!string.IsNullOrEmpty(p) && value.StartsWith(p))
                    {
                        return;
                    }
                }
            }

            workingNode.CreateUidOrNameIfNecessary(isInResource);
            string resName = string.Concat(workingNode.NameUid, "_", attributeName);
            this.ListResource.Add(new ResourceDataItem(resName, value));
            this.NumberProperties++;
            if (byBinding)
            {
                // generate alias if necessary
                if (!this.IsAliasResourceGenerated)
                {
                    this.GenerateAliasForResource(this.ResXClassName, this.ResXNamespace);
                }

                this.NumberAddBinding++;
                string newValue = global.BindingString;
                if (string.IsNullOrEmpty(newValue))
                    newValue = global.DefaultStringBinding;
                newValue = Utils.ReplaceNamedParams(newValue, this.replaceParams);
                newValue = newValue.Replace("%Name%", resName);
                xl.InnerXml = newValue;
            }

            if (workingNode.NameThis != null)
            {
                this.TranslatLines.Add(new TranslatLine(workingNode.NameThis, attributeName, resName));
            }
        }
    }
}
